﻿using System;
using System.Net;
using System.Net.Sockets;

using NATP2P.NATExceptionHandler;

namespace NATP2P.STUN
{
    /// <summary>
    /// This <see cref="T:NATP2P.STUN.EndPointMapRequest" />  Class represent  information like public ip address and public port
    /// and private ip address and private port
    /// </summary>
    public class EndPointMapRequest : IDisposable, IEndPointMapRequest
    {


        private IPEndPoint _externalEndPoint;
        /// <summary>
        /// Get or Set Public IP Address And Port <see cref="T:System.Net.IPEndPoint" />
        /// </summary>
        public IPEndPoint ExternalEndPoint
        {
            get
            {
                if (_externalEndPoint == null)
                {
                    throw new PropertyNullException("You need to send request first before use EndPointMapRequest.ExternalEndPoint");
                }
                return _externalEndPoint;
            }
            set { _externalEndPoint = value; }
        }

        private IPEndPoint _internalEndPoint;
        /// <summary>
        /// Get or Set Local IP Address And Port <see cref="T:System.Net.IPEndPoint" />
        /// </summary>
        public IPEndPoint InternalEndPoint
        {
            get { return _internalEndPoint; }
            set { _internalEndPoint = value; }
        }

        private Socket _initSocket;
        /// <summary>
        /// Get  Intinal Socket That Used in The First Sending <see cref="T:System.Net.Sockets.Socket" />
        /// </summary>
        public Socket InitSocket
        {
            get { return _initSocket; }
            private set { _initSocket = value; }
        }


        private IPEndPoint _remoteServer;
        /// <summary>
        /// Get Remote IP Adress And Port Used In Connection To Server <see cref="T:System.Net.IPEndPoint" />
        /// </summary>
        public IPEndPoint RemoteServer
        {
            get { return _remoteServer; }
            private set { _remoteServer = value; }
        }

        /// <summary>
        /// Prepare Class For  Send and Receive 
        /// </summary>
        /// <param name="SourceAddress">Your Local Ip Address <see cref="T:System.String" /></param>
        /// <param name="SourcePort">Your Local Port That The information outgoing <see cref="T:System.Int" /></param>
        /// <param name="url">url of stun server <see cref="T:System.String" /></param>
        public EndPointMapRequest(string SourceAddress, int SourcePort, string url)
        {
            if ((SourceAddress == "") || (SourcePort == 0) || (url == ""))
                throw new ArgumentNullException("You Must Specify Source Address and Port");
            this.InternalEndPoint = new IPEndPoint(IPAddress.Any, SourcePort);
            this.RemoteServer = new IPEndPoint(Dns.GetHostEntry(url).AddressList[0], 3478);
        }
        /// <summary>
        /// set local endpoint to get endpoint map to
        /// </summary>
        /// <param name="LocalEndPoint">use local end point </param>
        /// <param name="url">url of stun server <see cref="T:System.String" /></param>
        public EndPointMapRequest(IPEndPoint LocalEndPoint, string url)
        {
            if ((LocalEndPoint == null) || (url == ""))
                throw new ArgumentNullException("You Must Specify Source Address and Port");
            this.InternalEndPoint = LocalEndPoint;
            this.RemoteServer = new IPEndPoint(Dns.GetHostEntry(url).AddressList[0], 3478);
        }

        /// <summary>
        /// Prepare Class For  Send and Receive 
        /// </summary>
        /// <param name="SourcePort">Your Local Port That The information outgoing</param>
        /// <param name="url">url of stun server <see cref="T:System.String" /></param>
        public EndPointMapRequest(int SourcePort, string url)
        {
            if ((SourcePort == 0) || (url == ""))
                throw new ArgumentNullException("You Must Specify Source  Port");

            this.InternalEndPoint = new IPEndPoint(IPAddress.Any, SourcePort);
            this.RemoteServer = new IPEndPoint(Dns.GetHostEntry(url).AddressList[0], 3478);

        }

        /// <summary>
        /// Default Constructor
        /// </summary>
        public EndPointMapRequest()
        {

        }
        /// <summary>
        /// Clas to find private ip address
        /// </summary>
        /// <returns>Get Local ip address</returns>
        private string GetLocalIP()
        {
            IPHostEntry host;
            string localIP = "?";
            host = Dns.GetHostEntry(Dns.GetHostName());
            foreach (IPAddress ip in host.AddressList)
            {
                if (ip.AddressFamily == AddressFamily.InterNetwork)
                {
                    localIP = ip.ToString();
                }
            }
            return localIP;
        }
        private string GetIP4Address()
        {
            string IP4Address = String.Empty;

            foreach (IPAddress IPA in Dns.GetHostAddresses(Dns.GetHostName()))
            {
                if (IPA.AddressFamily.ToString() == "InterNetwork")
                {
                    IP4Address = IPA.ToString();
                    break;
                }
            }

            return IP4Address;
        }

        /// <summary>
        /// Send Packet To stun server for getting response contain endpoint map
        /// </summary>
        public void Send()
        {
            if (this.InternalEndPoint == null)
                throw new PropertyNullException("You must Specify Source Port or Source endpoint at Constructor");

            // UdpClient udpSocket = new UdpClient();
            // udpSocket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            // udpSocket.Client.Bind(this.InternalEndPoint);
            // udpSocket.Connect(StunServers.Server[0]);
            // udpSocket.Send(packet, packet.Length);
            Packet stunPacket = new Packet();


            Socket udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            udpSocket.Bind(this.InternalEndPoint);

            byte[] packet = stunPacket.CreatePacket();

            udpSocket.SendTo(packet, this.RemoteServer);
            this.RemoteServer = new IPEndPoint(IPAddress.Any, 0);

            // Blocks until a message returns on this socket from a remote host.
            UdpClient dd = new UdpClient();
            dd.Client = udpSocket;
            Byte[] receiveBytes = dd.Receive(ref  _remoteServer);

            this.ExternalEndPoint = stunPacket.ParsePacket(receiveBytes);
            //  this.InitSocket = dd.Client;
            udpSocket.Dispose();
            dd.Close();

        }

        private bool disposed = false;
        /// <summary>
        /// Dispose And Close Initial Socket For Release Source Port
        /// </summary>

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);

        }

        private void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!this.disposed)
            {
                // If disposing equals true, dispose all managed
                // and unmanaged resources.
                if (disposing)
                {
                    // Dispose managed resources.
                    if (this.InitSocket != null)
                    {
                        this.InitSocket.Close();
                    }
                }

                // Note disposing has been done.
                disposed = true;

            }
        }
    }
}
